import { fileTypeFromBuffer } from "./filetype"
import { CancellationOptions } from "./cancellation"
import { deleteUndefinedValues } from "./cleaners"
import { createFetch } from "./fetch"
import { runtimeHost } from "./host"
import { HTMLEscape } from "./htmlescape"
import { TraceOptions } from "./trace"
import { logError, logVerbose } from "./util"
import { dedent } from "./indent"
import { TOOL_ID } from "./constants"
import { filenameOrFileToFilename } from "./unwrappers"
import { resolveFileBytes } from "./file"
import { basename } from "node:path"
import { frontmatterTryParse, splitMarkdown } from "./frontmatter"

/**
 * Converts a Markdown string into HTML formatted for Microsoft Teams.
 *
 * @param markdown - The Markdown content to be converted.
 * Supports headers, lists, links, bold, italic, underlined, code, strikethrough, and blockquotes.
 *
 * @returns An object containing:
 * - `content`: The converted HTML string suitable for Microsoft Teams.
 * - `subject`: The extracted title if available, or undefined.
 */
export function convertMarkdownToTeamsHTML(markdown: string) {
    // using regexes, convert headers, lists, links, bold, italic, code, and quotes
    const { content, frontmatter } = splitMarkdown(markdown || "")
    const fm = frontmatterTryParse(frontmatter)
    let subject = fm?.value?.title as string
    const html =
        "<div>" +
        (content || "")
            .replace(/^# (.*$)/gim, (m, t) => {
                subject = t
                return ""
            })
            .replace(/^#### (.*$)/gim, "<h3>$1</h3>")
            .replace(/^### (.*$)/gim, "<h2>$1</h2>")
            .replace(/^## (.*$)/gim, "<h1>$1</h1>")
            .replace(/^\> (.*$)/gim, "<blockquote>$1</blockquote>\n")
            .replace(/\*\*(.*)\*\*/gim, "<b>$1</b>")
            .replace(/\*(.*)\*/gim, "<i>$1</i>")
            .replace(/__(.*)__/gim, "<u>$1</u>")
            .replace(/`(.*?)`/gim, "<code>$1</code>")
            .replace(/~~(.*?)~~/gim, "<strike>$1</strike>")
            .replace(/^- (.*$)/gim, "<br/>- $1") +
        "</div>"
    return { content: html.trim(), subject: subject?.trim() }
}

function parseTeamsChannelUrl(url: string) {
    const m =
        /^https:\/\/teams.microsoft.com\/[^\/]{1,32}\/channel\/(?<channelId>.+)\/.*\?groupId=(?<teamId>([a-z0-9\-])+)$/.exec(
            url
        )
    if (!m) throw new Error("Invalid Teams channel URL")
    const { teamId, channelId } = m.groups
    return { teamId, channelId }
}

export interface MicrosoftTeamsEntity {
    webUrl: string
    name: string
}

function generatedByFooter(script: PromptScript, info: { runUrl?: string }) {
    if (!script)
        return `\n<blockquote>AI-generated may be incorrect</blockquote>\n`
    return `\n<blockquote>AI-generated by ${info?.runUrl ? `<a href="${HTMLEscape(info.runUrl)}">${HTMLEscape(script.id)}</a>` : HTMLEscape(script.id)} may be incorrect</blockquote>\n`
}

/**
 * Uploads a file to the files storage of a Microsoft Teams channel.
 * @param channelUrl Shared channel link in the format https://teams.microsoft.com/l/channel/<channelId>/<channelName>?groupId=<teamId>
 * @param filename
 * @returns
 */
async function microsoftTeamsChannelUploadFile(
    token: string,
    channelUrl: string,
    file: string | WorkspaceFileWithDescription,
    options?: { folder?: string; disclaimer?: string } & TraceOptions &
        CancellationOptions
): Promise<MicrosoftTeamsEntity> {
    const { disclaimer } = options || {}

    const filename = filenameOrFileToFilename(file)
    const description = typeof file === "object" ? file.description : undefined
    logVerbose(`teams: uploading ${filename}...`)

    const { teamId, channelId } = parseTeamsChannelUrl(channelUrl)
    const Authorization = `Bearer ${token}`

    const channelInfoUrl = `https://graph.microsoft.com/v1.0/teams/${teamId}/channels/${channelId}`
    const fetch = await createFetch({ ...(options || {}), retries: 1 })
    const channelInfoRes = await fetch(channelInfoUrl, {
        headers: {
            Authorization,
        },
    })
    if (!channelInfoRes.ok) {
        throw new Error(
            `Failed to get channel info: ${channelInfoRes.status} ${channelInfoRes.statusText}`
        )
    }
    const channelInfo = await channelInfoRes.json()
    const root = channelInfo.displayName

    // resolve channel folder name
    const content = await resolveFileBytes(file, options)
    if (!file) throw new Error(`${filename} not found`)
    const folder = options?.folder || TOOL_ID
    const itemUrl = `https://graph.microsoft.com/v1.0/groups/${teamId}/drive/root:/${root}/${folder}/${basename(
        filename
    )}`
    const contentUrl = `${itemUrl}:/content`
    const mime = await fileTypeFromBuffer(content)
    const res = await fetch(contentUrl, {
        method: "PUT",
        headers: {
            Authorization,
            "Content-Type": mime?.mime || "application/octet-stream",
        },
        body: content,
    })
    if (!res.ok) {
        logError(await res.text())
        throw new Error(
            `Failed to upload file: ${res.status} ${res.statusText}`
        )
    }
    const j = (await res.json()) as MicrosoftTeamsEntity
    logVerbose(`teams: uploaded ${filename} to ${j.webUrl}`)

    if (disclaimer || description) {
        const html = convertMarkdownToTeamsHTML(description)
        if (disclaimer) html.content += disclaimer

        const dbody = deleteUndefinedValues({
            description: html.content,
            title: html.subject,
        })
        const resd = await fetch(itemUrl, {
            method: "PATCH",
            headers: {
                Authorization,
                "Content-Type": "application/json",
            },
            body: JSON.stringify(dbody),
        })
        if (!resd.ok) {
            logVerbose(`description: ${dbody.description}`)
            logVerbose(await resd.json())
            throw new Error(
                `Failed to update file description: ${resd.status} ${resd.statusText}`
            )
        }
    }

    return j
}

/**
 * Sends a message to a Microsoft Teams channel, optionally including file attachments.
 *
 * @param channelUrl The URL of the Microsoft Teams channel, in the format https://teams.microsoft.com/l/channel/<channelId>/<channelName>?groupId=<teamId>.
 * @param message The message content to post in Markdown format. It will be converted to HTML.
 * @param options Additional configurations for the message.
 *   - script: The script context to include an attribution footer.
 *   - info: Metadata about the originating script or run (e.g., runUrl).
 *   - files: A list of files to attach to the message. Files are uploaded to the channel's storage.
 *   - folder: The folder in the channel's storage where files will be uploaded, if applicable.
 *   - disclaimer: A disclaimer to append to the message. If a string, it is used directly. Pass false to omit it.
 * @returns A promise resolving to the created message entity containing the message's metadata, including its web URL.
 */
export async function microsoftTeamsChannelPostMessage(
    channelUrl: string,
    message: string,
    options?: {
        script?: PromptScript
        info?: { runUrl?: string }
        files?: (string | WorkspaceFileWithDescription)[]
        folder?: string
        disclaimer?: boolean | string
    } & TraceOptions &
        CancellationOptions
): Promise<MicrosoftTeamsEntity> {
    logVerbose(`teams: posting message to ${channelUrl}`)

    const { files = [] } = options || {}
    const { teamId, channelId } = parseTeamsChannelUrl(channelUrl)
    const authToken = await runtimeHost.microsoftGraphToken.token("default")
    const token = authToken?.token?.token
    if (!token) {
        logError("Microsoft Graph token not available")
        return undefined
    }

    // convert message to html
    const { content, subject } = convertMarkdownToTeamsHTML(message)
    const disclaimer =
        typeof options.disclaimer === "string"
            ? `\n<blockquote>${HTMLEscape(options.disclaimer)}</blockquote>\n`
            : options.disclaimer !== false
              ? generatedByFooter(options?.script, options?.info)
              : undefined

    const body = deleteUndefinedValues({
        body: {
            contentType: "html",
            content,
        },
        subject,
        attachments: [] as any[],
    })

    for (const file of files) {
        const fres = await microsoftTeamsChannelUploadFile(
            token,
            channelUrl,
            file,
            {
                ...options,
                disclaimer,
            }
        )
        const guid = crypto.randomUUID()
        body.body.content += "\n" + `<attachment id=\"${guid}\"></attachment>`
        body.attachments.push({
            id: guid,
            contentType: "reference",
            contentUrl: fres.webUrl,
            name: fres.name,
            thumbnailUrl: null,
        })
    }

    // finalize message
    if (disclaimer) body.body.content += disclaimer

    const url = `https://graph.microsoft.com/v1.0/teams/${teamId}/channels/${channelId}/messages`
    const fetch = await createFetch({ ...(options || {}), retries: 1 })
    const response = await fetch(url, {
        method: "POST",
        headers: {
            Authorization: `Bearer ${token}`,
            "Content-Type": "application/json",
        },
        body: JSON.stringify(body),
    })

    if (!response.ok) {
        const err: any = await response.text()
        logError(err)
        return undefined
    }

    const data: any = await response.json()
    const { webUrl } = data
    logVerbose(`teams: message created at ${webUrl}`)
    return data
}

class MicrosoftTeamsChannelClient implements MessageChannelClient {
    constructor(public readonly channelUrl: string) {}

    get teamId() {
        const { teamId } = parseTeamsChannelUrl(this.channelUrl)
        return teamId
    }

    get channelId() {
        const { channelId } = parseTeamsChannelUrl(this.channelUrl)
        return channelId
    }

    /**
     * Posts a message with attachments to the channel
     * @param message
     * @param options
     */
    async postMessage(
        message: string,
        options?: {
            /**
             * File attachments that will be added in the channel folder
             */
            files?: string[]
            /**
             * Sets to false to remove AI generated disclaimer
             */
            disclaimer?: boolean | string
        }
    ): Promise<string> {
        const { files, disclaimer } = options || {}
        const res = await microsoftTeamsChannelPostMessage(
            this.channelUrl,
            dedent(message),
            {
                files,
                disclaimer,
            }
        )
        return res.webUrl
    }

    toString() {
        return this.channelUrl
    }
}

/**
 * Creates a Microsoft Teams Channel Client for interacting with a Teams channel.
 * This client allows posting messages and managing attachments in the specified channel.
 *
 * @param url The URL of the Microsoft Teams channel. Must be in the format:
 *            `https://teams.microsoft.com/.../channel/<channelId>/<channelName>?groupId=<teamId>`.
 *            If not provided, the function attempts to retrieve the URL from the
 *            GENAISCRIPT_TEAMS_CHANNEL_URL or GENAISCRIPT_TEAMS_URL environment variables.
 *
 * @throws Error if the provided URL is invalid or cannot be parsed.
 *
 * @returns An instance of a MicrosoftTeamsChannelClient for interacting with the specified channel.
 */
export function createMicrosoftTeamsChannelClient(
    url: string
): MessageChannelClient {
    if (!url)
        url =
            process.env.GENAISCRIPT_TEAMS_CHANNEL_URL ||
            process.env.GENAISCRIPT_TEAMS_URL
    if (!parseTeamsChannelUrl(url)) throw new Error("Invalid Teams channel URL")
    return new MicrosoftTeamsChannelClient(url)
}
